package br.edu.ufcg.greengrid.model.machine;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import br.edu.ufcg.greengrid.model.Grid;
import br.edu.ufcg.greengrid.model.job.Job;
import br.edu.ufcg.greengrid.model.job.JobWithCheckpoint;
import br.edu.ufcg.greengrid.model.machine.state.SleepGoingGreenMachineState;
import br.edu.ufcg.greengrid.model.machine.state.SleepLeavingGreenMachineState;
import br.edu.ufcg.greengrid.model.machine.state.StandbyGoingGreenMachineState;
import br.edu.ufcg.greengrid.model.machine.state.StandbyLeavingGreenMachineState;
import br.edu.ufcg.greengrid.model.machine.state.MachineState.MSEnum;
import br.edu.ufcg.greengrid.model.machine.state.factory.MachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.NormalMachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.SleepMachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.StandByMachineStateFactory;

public class MachineTest {

	private Machine m;
	
	private MachineStateFactory nmsf = new NormalMachineStateFactory();
	
	private MachineStateFactory smsf = new SleepMachineStateFactory();
	
	private MachineStateFactory sbmsf = new StandByMachineStateFactory();

	private static class FakeGrid extends Grid {
		
		public void machineChangedState(Machine m, MSEnum oldStateType, MSEnum newStateType) {
			
		}
		
	}
	
	@Before
	public void setUp() throws Exception {

		m = getMachine(nmsf);
	}
	
	private Machine getMachine(MachineStateFactory msf){
		
		return new Machine(new FakeGrid(), "machine1", msf.getStates(), Machine.DEFAULT_OPERATIONS_PER_TIME);
	}
	
	private Machine getMachine(MachineStateFactory msf, long operationsPerSecond){
		
		return new Machine(new FakeGrid(), "machine1", msf.getStates(), operationsPerSecond);
	}

	@After
	public void tearDown() throws Exception {

	}

	@Test
	public void testIdleMachine() {

		assertFalse(m.isIdle());

		long idletime = 100;
		m.setIdle(idletime);
		assertTrue(m.isIdle());

		for (int i = 0; i < idletime - 1; i++) {
			m.update();
			assertTrue(m.isIdle());
		}

		m.update();
		assertFalse(m.isIdle());
	}

	@Test
	public void testSetIdleMachineThatIsAlreadyIdle() {
		
		long idletime = 10;
		
		m.setIdle(idletime);
		m.setIdle(idletime);
		
		for (int i = 0; i < idletime; i++) {
			m.update();
		}
		
		assertFalse(m.isIdle());
		
	}
	
	@Test
	public void testIdleMachineWithJobAllocated() {

		long runtime = 3;
		Job j = new Job(runtime);

		long idletime = 5;
		m.setIdle(idletime);
		m.run(j);

		for (int i = 0; i < runtime - 1; i++) {
			m.update();
			assertFalse(m.isIdle());
			assertFalse(j.done());
		}

		m.update();
		assertTrue(m.isIdle());
		assertTrue(j.done());

		for (long i = runtime; i < idletime - 1; i++) {
			m.update();
			assertTrue(String.format("i = %d", i), m.isIdle());
		}

		m.update();
		assertTrue(m.isBeingUsedByUser());
		assertFalse(m.isIdle());
	}

	@Test
	public void testIsMachineBeingUsedByUser() {

		assertTrue(m.isBeingUsedByUser());

		long idletime = 10;
		m.setIdle(idletime);

		assertFalse(m.isBeingUsedByUser());

		for (int i = 0; i < idletime - 1; i++) {
			m.update();
			assertFalse(m.isBeingUsedByUser());
		}

		m.update();
		assertTrue(m.isBeingUsedByUser());

	}

	@Test
	public void testHasJobAllocated() {

		assertFalse(m.hasJobAllocated());

		long idletime = 15;
		m.setIdle(idletime);

		assertFalse(m.hasJobAllocated());

		long runtime = 10;
		Job j = new Job(runtime);

		m.run(j);
		assertTrue(m.hasJobAllocated());

		for (int i = 0; i < runtime - 1; i++) {
			m.update();
			assertTrue(m.hasJobAllocated());
		}

		m.update();
		assertFalse(m.hasJobAllocated());
		assertTrue(j.done());
	}

	@Test
	public void testIsDone() {

		assertTrue(m.done());

		long runtime = 3;
		Job j = new Job(runtime);

		long idletime = 5;
		m.setIdle(idletime);
		m.run(j);

		assertFalse(m.done());

		for (int i = 0; i < runtime - 1; i++) {
			m.update();
			assertFalse(String.format("i = %d", i), m.done());
		}

		m.update();
		assertTrue(m.done());

		for (long i = runtime; i < idletime - 1; i++) {
			m.update();
			assertTrue(m.done());
		}

		m.update();
		assertTrue(m.done());
	}

	@Test
	public void testReleaseJob() {

		long runtime = 10;
		Job j = new Job(runtime);

		long idletime = 15;
		m.setIdle(idletime);
		m.run(j);

		for (int i = 0; i < runtime; i++) {
			m.update();
		}

		assertFalse(m.hasJobAllocated());
		assertTrue(j.done());

	}

	@Test
	public void testRunJobWithoutBeingIdle() {

		long runtime = 10;
		Job j = new Job(runtime);
		try {
			m.run(j);
			fail("Failure! Expected an IllegalStateException but nothing happened");
		} catch (IllegalStateException e) {
		}
	}

	@Test
	public void testRunJobInAlreadyAllocatedMachine() {

		m.setIdle(15);
		Job j = new Job(10);
		m.run(j);

		Job j2 = new Job(10);
		try {
			m.run(j2);
			fail("Failure! Expected an IllegalStateException but nothing happened");
		} catch (IllegalStateException e) {
		}
	}

	@Test
	public void testRunJobInTwoMachineWithDifferentCapabilities() {
		
		
		long runtime = 130;
		long timeToOperationsFactor = 1;
		Job j = new JobWithCheckpoint(0L, runtime, timeToOperationsFactor);
		
		long operationsPerSecond1 = 2L;
		long idletime1 = 25;
		m = getMachine(nmsf, operationsPerSecond1);
		m.setIdle(idletime1);
		m.run(j);
		
		for (long i = 0; i < idletime1; i++) {
			assertFalse("i = " + i, j.done());
			m.update();
		}
		
		assertFalse(m.hasJobAllocated());
		assertFalse(j.done());
		
		//20
		long operationsPerSecond2 = 4L;
		long idletime2 = 20;
		m = getMachine(nmsf, operationsPerSecond2);
		m.setIdle(idletime2);
		m.run(j);
		
		for (long i = 0; i < idletime2; i++) {
			assertFalse("i = " + i, j.done());
			m.update();
		}
		
		assertFalse(m.hasJobAllocated());
		assertTrue(j.done());
	}
	
	@Test
	public void testRunJobInTwoMachineWithDifferentCapabilitiesMoreComplexCase() {
		
		long runtime = 130;
		long timeToOperationsFactor = 177120;
		Job j = new JobWithCheckpoint(0L, runtime, timeToOperationsFactor);
		
		//65 seconds of computation from 'runtime'
		long operationsPerSecond1 = 110700;
		long idletime1 = 104;
		m = getMachine(nmsf, operationsPerSecond1);
		m.setIdle(idletime1);
		m.run(j);
		
		for (long i = 0; i < idletime1; i++) {
			assertFalse("i = " + i, j.done());
			m.update();
		}
		
		assertFalse(m.hasJobAllocated());
		assertFalse(j.done());
		
		//65 seconds of computation from 'runtime'
		long operationsPerSecond2 = 160000;
		long idletime2 = 72;
		m = getMachine(nmsf, operationsPerSecond2);
		m.setIdle(idletime2);
		m.run(j);
		
		for (long i = 0; i < idletime2; i++) {
			assertFalse("i = " + i, j.done());
			m.update();
		}
		
		assertFalse(m.hasJobAllocated());
		assertTrue(j.done());
		
	}
	
	@Test
	public void testRunJobWithMachineCapableOfRunningMoreThanOneOperationPerSecond() {
		
		assertRunJobWithMachineCapableOfRunningMoreThanOneOperationPerSecond(1L, 1L, 100L);
		assertRunJobWithMachineCapableOfRunningMoreThanOneOperationPerSecond(1L, 10L, 100L);
		assertRunJobWithMachineCapableOfRunningMoreThanOneOperationPerSecond(2L, 10L, 100L);
		
	}
	
	private void assertRunJobWithMachineCapableOfRunningMoreThanOneOperationPerSecond(long timeToOperationsFactor, long operationsPerSecond, long runtime){

		long idletime = runtime * operationsPerSecond * timeToOperationsFactor + 1;
		
		m = getMachine(nmsf, operationsPerSecond);
		
		Job j = new Job(0L, runtime, timeToOperationsFactor);

		m.setIdle(idletime);
		m.run(j);
		
		long expectedRuntime = (long) Math.ceil(1.0*timeToOperationsFactor*runtime/operationsPerSecond);
		for (long i = 0; i < expectedRuntime; i++) {
			assertFalse("i = " + i, j.done());
			m.update();
		}
		
		assertFalse(m.hasJobAllocated());
		assertTrue(j.done());
	}
	
	@Test
	public void testTimeSpentAtStateUserWithMachineInNormalMode() {
		
		long expectedTimeSpent = 100;
		for (int i = 0; i < expectedTimeSpent; i++) {
			m.update();
		}
		
		assertEquals(expectedTimeSpent, m.getTotalTimeSpentAtState(MSEnum.USER));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.IDLE));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GRID));
		
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GOING_GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.LEAVING_GREEN));
	}
	
	@Test
	public void testTimeSpentAtStateIdleWithMachineInNormalMode() {
		
		long expectedTimeSpent = 100;
		m.setIdle(expectedTimeSpent);
		
		for (int i = 0; i < expectedTimeSpent; i++) {
			m.update();
		}
		
		assertEquals(expectedTimeSpent, m.getTotalTimeSpentAtState(MSEnum.IDLE));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.USER));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GRID));
		
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GOING_GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.LEAVING_GREEN));
	}
	
	@Test
	public void testTimeSpentAtStateGridWithMachineInNormalMode(){
		
		long expectedTimeSpent = 100;
		m.setIdle(expectedTimeSpent);
		m.run(new Job(expectedTimeSpent));
		
		for (int i = 0; i < expectedTimeSpent; i++) {
			m.update();
		}
		
		assertEquals(expectedTimeSpent, m.getTotalTimeSpentAtState(MSEnum.GRID));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.USER));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.IDLE));
		
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GOING_GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.LEAVING_GREEN));
	}
	
	@Test
	public void testTimeSpentAtStateGoingGreenWithMachineInSleepMode(){
		
		testTimeSpentAtStateGoingGreen(smsf, SleepLeavingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testTimeSpentAtStateGreenWithMachineInSleepMode() {
		
		testTimeSpentAtStateGreen(smsf, SleepLeavingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testTimeSpentAtStateLeavingGreenWithMachineInSleepMode() {
		
		testTimeSpentAtStateLeavingGreen(smsf, SleepLeavingGreenMachineState.WAIT_TIME);
	}

	@Test
	public void testTimeSpentAtStateGoingGreenWithMachineInStandByMode(){
		
		testTimeSpentAtStateGoingGreen(sbmsf, StandbyGoingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testTimeSpentAtStateGreenWithMachineInStandByMode() {
		
		testTimeSpentAtStateGreen(sbmsf, StandbyGoingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testTimeSpentAtStateLeavingGreenWithMachineInStandByMode() {
		
		testTimeSpentAtStateLeavingGreen(sbmsf, StandbyGoingGreenMachineState.WAIT_TIME);
	}
	
	private void testTimeSpentAtStateGoingGreen(MachineStateFactory msf, long waitTime){
		
		m = getMachine(msf);
		long idletime = 1000;
		long expectedTimeSpent = waitTime;
		
		m.setIdle(idletime);
		
		long delta = waitTime + 1;
		assertTrue(delta > waitTime);
		
		for (int i = 0; i < delta; i++) {
			m.update();
		}
		
		assertEquals(expectedTimeSpent, m.getTotalTimeSpentAtState(MSEnum.GOING_GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.IDLE));
	}
	
	private void testTimeSpentAtStateGreen(MachineStateFactory msf, long waitTime) {
		
		m = getMachine(msf);
		long idletime = 1000;
		long expectedTimeSpent = waitTime;
		
		m.setIdle(idletime);
		
		long delta = waitTime + 1;
		assertTrue(delta > waitTime);
		
		for (int i = 0; i < delta; i++) {
			m.update();
		}
		
		assertEquals(delta - expectedTimeSpent, m.getTotalTimeSpentAtState(MSEnum.GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.IDLE));
	}
	
	private void testTimeSpentAtStateLeavingGreen(MachineStateFactory msf, long waitTime) {
		
		m = getMachine(msf);
		long idletime = 1000;
		long expectedTimeSpent = waitTime;
		
		m.setIdle(idletime);
		
		long delta = waitTime + 1;
		assertTrue(delta > waitTime);
		
		for (int i = 0; i < delta; i++) {
			m.update();
		}
		
		m.wakeUp();
		
		for (int i = 0; i < waitTime; i++) {
			m.update();
		}
		
		Job j = new Job(delta);
		m.run(j);

		for (int i = 0; i < delta; i++) {
			m.update();
		}
		
		assertTrue(j.done());
		assertEquals(expectedTimeSpent, m.getTotalTimeSpentAtState(MSEnum.LEAVING_GREEN));
		assertEquals(0L, m.getTotalTimeSpentAtState(MSEnum.IDLE));
		
	}
	
	@Test
	public void testPreemptingMachineAtStateIdleWithMachineInNormalMode(){
		
		m = getMachine(nmsf);
		long idleTime = 100;
		m.setIdle(idleTime);
		
		for (long i = 0; i < idleTime; i++) {
			assertTrue(String.format("i = %d", i), m.isAtState(MSEnum.IDLE));
			m.update();
		}
		
		assertTrue(String.format("current state: %s", m.getCurrentStateType()), m.isAtState(MSEnum.USER));
	}

	@Test
	public void testPreemptingMachineAtStateGridWithMachineInNormalMode(){
		
		m = getMachine(nmsf);
		long idleTime = 100;
		m.setIdle(idleTime);
		
		long runtime = 2 * idleTime;
		Job j = new Job(runtime);
		m.run(j);
		
		for (long i = 0; i < idleTime; i++) {
			assertTrue(String.format("i = %d", i), m.isAtState(MSEnum.GRID));
			m.update();
		}
		
		assertFalse(j.done());
		assertTrue(String.format("current state: %s", m.getCurrentStateType()), m.isAtState(MSEnum.USER));
	}
	
	@Test
	public void testPreemptingMachineAtStateGoingGreenWithMachineInSleepMode(){
		
		testPreemptingMachineAtStateGoingGreen(smsf, SleepGoingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testPreemptingMachineAtStateGreenWithMachineInSleepMode(){
		
		testPreemptingMachineAtStateGreen(smsf, SleepGoingGreenMachineState.WAIT_TIME);
	}

	@Test
	public void testPreemptingMachineAtStateLeavingGreenWithMachineInSleepMode(){
		
		testPreemptingMachineAtStateLeavingGreen(smsf, SleepGoingGreenMachineState.WAIT_TIME, SleepLeavingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testPreemptingMachineAtStateGoingGreenWithMachineInStandbyMode(){
		
		testPreemptingMachineAtStateGoingGreen(sbmsf, StandbyGoingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testPreemptingMachineAtStateGreenWithMachineInStandbyMode(){
		
		testPreemptingMachineAtStateGreen(sbmsf, StandbyGoingGreenMachineState.WAIT_TIME);
	}
	
	@Test
	public void testPreemptingMachineAtStateLeavingGreenWithMachineInStandbyMode(){
		
		testPreemptingMachineAtStateLeavingGreen(sbmsf, StandbyGoingGreenMachineState.WAIT_TIME, StandbyLeavingGreenMachineState.WAIT_TIME);
	}
	
	private void testPreemptingMachineAtStateGoingGreen(MachineStateFactory msf, long waitTime){

		m = getMachine(msf);
		long idleTime = waitTime - 1;
		m.setIdle(idleTime);
		
		for (long i = 0; i < idleTime - 1; i++) {
			m.update();
			assertTrue(String.format("i = %d", i), m.isAtState(MSEnum.GOING_GREEN));
		}
		
		m.update();
		assertEquals(idleTime, m.getTotalTimeSpentAtState(MSEnum.GOING_GREEN));
		
		assertTrue(String.format("current state: %s", m.getCurrentStateType()), m.isAtState(MSEnum.USER));
	}
	
	private void testPreemptingMachineAtStateGreen(MachineStateFactory msf, long waitTime){

		m = getMachine(msf);
		long idleTime = waitTime + 1;
		m.setIdle(idleTime);
		
		for (long i = 0; i < waitTime; i++) {
			m.update();
		}
		
		for (long i = waitTime; i < idleTime; i++) {
			assertTrue(String.format("i = %d", i), m.isAtState(MSEnum.GREEN));
			m.update();
		}
		
		assertEquals(idleTime - waitTime, m.getTotalTimeSpentAtState(MSEnum.GREEN));
		
		assertTrue(String.format("current state: %s", m.getCurrentStateType()), m.isAtState(MSEnum.USER));
	}
	
	private void testPreemptingMachineAtStateLeavingGreen(MachineStateFactory msf, long waitTimeGoingGreen, long waitTimeLeavingGreen){
	
		m = getMachine(msf);
		long idleTime = waitTimeGoingGreen + waitTimeLeavingGreen - 1;
		m.setIdle(idleTime);
		
		for (long i = 0; i < waitTimeGoingGreen; i++) {
			m.update();
		}
		
		m.wakeUp();
		
		for (long i = waitTimeGoingGreen; i < idleTime; i++) {
			assertTrue(String.format("i = %d", i), m.isAtState(MSEnum.LEAVING_GREEN));
			m.update();
		}
		
		assertEquals(idleTime - waitTimeGoingGreen, m.getTotalTimeSpentAtState(MSEnum.LEAVING_GREEN));
		
		assertTrue(String.format("current state: %s", m.getCurrentStateType()), m.isAtState(MSEnum.USER));
	}
	
}
